/**
 * @file        application.cpp
 * @brief       Defines the GTD application barebones.
 *
 * @author      Emre Turkay <emreturkay at gmail>
 * @date        Jun.04.2008
 *
 * Copyright Emre Turkay 2009
 * Distributed under the Boost Software License, Version 1.0.
 * (See accompanying file COPYRIGHT or copy at
 * http://www.boost.org/LICENSE_1_0.txt)
 */

#include "application.hpp"
#include "interface.hpp"
#include "gtd.hpp"
#include "rl.hpp"
#include "configuration.hpp"

#include <boost/foreach.hpp>
#define foreach BOOST_FOREACH

#include <stdexcept>

#include <unistd.h>
#include <sys/wait.h>

struct usage_error: std::runtime_error
{
    usage_error(const std::string& msg)
        : std::runtime_error(msg)
    {
    }
};

/**
 * @defgroup gtd-app-commands Functions implementing the requested commands.
 * @{
 */

namespace commands
{
    /// The "next" command handler, prints the next task.
    struct next
    {
        next(application& app)
            : app_(app)
        {
        }

        void operator()(const interface::args_type& arg)
        {
            if (arg.size())
                throw usage_error("next");

            app_.prompt();
        }

    private:
        application& app_;
    };

    /// The "done" command handler, marks the current task as done.
    struct done
    {
        done(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
             gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type& arg)
        {
            if (arg.size())
                throw usage_error("done");

            if (pid_)
            {
                backend_.done(*pid_);
                intf_.write("done");
                app_.prompt();
            }
            else
                intf_.write("There is no item in the queue");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "cancel" command handler, removes the current task.
    struct cancel
    {
        cancel(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
               gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type& arg)
        {
            if (arg.size())
                throw usage_error("cancel");

            if (pid_)
            {
                backend_.cancel(*pid_);
                intf_.write("cancelled");
                app_.prompt();
            }
            else
                intf_.write("There is no item in the queue");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "defer" command handler, defers the next task to a later time.
    struct defer
    {
        defer(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
              gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type& arg)
        {
            if (arg.size())
                throw usage_error("defer");

            if (pid_)
            {
                backend_.defer(*pid_);
                intf_.write("deferred");
                app_.prompt();
            }
            else
                intf_.write("There is no item in the queue");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "append" command handler, adds a new task to the end of the queue.
    struct append
    {
        append(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
               gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type& args)
        {
            if (args.size() != 1)
                throw usage_error("append value");

            backend_.append(args[0]);
            intf_.write("Added.");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "prepend" command handler, adds a new task to the start of the queue.
    struct prepend
    {
        prepend(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
                gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type& args)
        {
            if (args.size() != 1)
                throw usage_error("prepend value");

            backend_.prepend(args[0]);
            intf_.write("Added.");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "context" command handler, sets/resets the current context.
    struct context
    {
        context(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
                gtd::gtd& backend, application& app, std::string& ctx)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app),
              context_(ctx)
        {
        }

        void operator()(const interface::args_type& args)
        {
            if (args.empty())
            {
                if (context_.size())
                {
                    std::ostringstream os;
                    os << "Context: " << context_;
                    intf_.write(os.str());
                    app_.prompt();
                }
                else
                    intf_.write("There is no context defined.");
            }
            else if ((args.size() == 1) && (args[0] == "-"))
            {
                context_ = "";
                intf_.write("Removed the context.");
                app_.prompt();
            }
            else if ((args.size() == 1) && (args[0][0] == '@'))
            {
                context_ = std::string(args[0].begin() + 1, args[0].end());
                std::ostringstream os;
                os << "Context is set to: " << context_;
                intf_.write(os.str());
                app_.prompt();
            }
            else
                throw usage_error("context [@context_name | -]");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
        std::string& context_;
    };

    /// The "stat" command handler, gives statistics about job done.
    struct stat
    {
        stat(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
             gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type&)
        {
            std::set<gtd::done_item> l =
                backend_.done_items(boost::gregorian::days(7));
            std::map<boost::gregorian::date, size_t> m;

            boost::gregorian::day_iterator
                i(boost::gregorian::day_clock::local_day()
                  - boost::gregorian::days(6));

            for (; i <= boost::gregorian::day_clock::local_day(); ++i)
                m[*i] = 0;

            foreach(const gtd::done_item& item, l)
                m[item.time().date()] += 1;

            for (std::map<boost::gregorian::date, size_t>::const_iterator
                 i = m.begin(); i != m.end(); ++i)
            {
                std::ostringstream os;
                os << i->first.day_of_week() << '\t' << i->second;
                intf_.write(os.str());
            }
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "list" command handler, gives a list of the next items.
    struct list
    {
        list(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
             gtd::gtd& backend, application& app)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app)
        {
        }

        void operator()(const interface::args_type& args)
        {
            if (args.size() == 0)
                list_all();
            else if ((args.size() == 1) && (args[0][0] == '+'))
            {
                std::string project_name(args[0].begin() + 1, args[0].end());
                boost::optional<gtd::gtd::project_id> id =
                    backend_.find_project(project_name);
                if (!id)
                    intf_.write("There is no such project.");
                else
                    list_project(*id);
            }
            else
                throw usage_error("list +project_name");
        }

    private:
        void list_all()
        {
            bool project_exists = false;
            size_t item_count = 0;
            size_t anonymous = 0;
            std::list<gtd::gtd::project_id> ids = backend_.project_ids();

            foreach(const gtd::gtd::project_id& id, ids)
            {
                const boost::optional<std::string>& name =
                    backend_.project_name(id);
                size_t items = backend_.item_count(id);
                if (name)
                {
                    std::ostringstream os;
                    os << *name << " (" << items << ')';
                    intf_.write(os.str());
                }
                else
                    ++anonymous;
                project_exists = true;
                item_count += items;
            }

            if (anonymous)
            {
                std::ostringstream os;
                os << "--anonymous-- (" << anonymous << ")";
                intf_.write(os.str());
            }

            if (!project_exists)
                intf_.write("There is no project.");
            else
            {
                std::ostringstream os;
                os << item_count << " total items.";
                intf_.write(os.str());
            }
        }

        void list_project(gtd::gtd::project_id id)
        {
            std::list<size_t> l = backend_.items(id);
            size_t n = 1;

            foreach(size_t i, l)
            {
                const gtd::action& action = backend_.item(id, i);
                std::ostringstream os;
                os << '[' << n++ << "] " << action.description();
                intf_.write(os.str());
            }
            
            std::ostringstream os;
            os << n-1 << " total items.";
            intf_.write(os.str());
        }    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
    };

    /// The "edit" command handler, opens an editor to view/edit tasks.
    struct edit
    {
        edit(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
             gtd::gtd& backend, application& app,
             const gtd::configuration& config)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app),
              config_(config)
        {
        }

        void operator()(const interface::args_type&)
        {
            backend_.save_todo(config_.tmp_file(), config_.backup_file());

            if (fork())
            {
                int status;
                wait(&status);
            }
            else
                execl("/usr/bin/vi", "vi", config_.tmp_file().c_str(), NULL);

            backend_.load_todo(config_.tmp_file());
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
        const gtd::configuration& config_;
    };

    /// The "quit" command handler, terminates the application loop.
    struct quit
    {
        quit(boost::optional<gtd::gtd::project_id>& pid, interface& intf,
             gtd::gtd& backend, application& app, bool& quit)
            : pid_(pid),
              intf_(intf),
              backend_(backend),
              app_(app),
              quit_(quit)
        {
        }

        void operator()(const interface::args_type& args)
        {
            if (args.size())
                throw usage_error("quit");

            quit_ = true;
            intf_.write("bye.");
        }

    private:
        boost::optional<gtd::gtd::project_id>& pid_;
        interface& intf_;
        gtd::gtd& backend_;
        application& app_;
        bool& quit_;
    };

};

/** @} */

application::application(interface& intf, gtd::gtd& backend,
                         const gtd::configuration& conf)
    : quit_(false),
      intf_(intf),
      backend_(backend),
      config_(conf)
{
    intf_.command("help", help_command(intf_),
                  "Show this help screen.");
    intf_.command("next", commands::next(*this),
                  "Show next action.");
    intf_.command("done", commands::done(pid_, intf_, backend_, *this),
                  "Mark the current action as done.");
    intf_.command("cancel", commands::cancel(pid_, intf_, backend_, *this),
                  "Cancel the current action.");
    intf_.command("defer", commands::defer(pid_, intf_, backend_, *this),
                  "Defer the current action.");
    intf_.command("append", commands::append(pid_, intf_, backend_, *this),
                  "Append item given in quotes.");
    intf_.command("prepend", commands::prepend(pid_, intf_, backend_, *this),
                  "Prepend item given in quotes.");
    intf_.command("context", commands::context(pid_, intf_, backend_, *this, context_),
                  "Set/remove/list contexts. context @ctx, context -, context");
    intf_.command("stat", commands::stat(pid_, intf_, backend_, *this),
                  "Give the statistics.");
    intf_.command("list", commands::list(pid_, intf_, backend_, *this),
                  "Lists the projects. If a project given lists the items.");
    intf_.command("edit", commands::edit(pid_, intf_, backend_, *this, config_),
                  "Edit actions in an external editor");
    intf_.command("quit", commands::quit(pid_, intf_, backend_, *this, quit_),
                  "Leave the gtdshell.");

    intf_.set_completer("stat", boost::bind(&application::context_complete, this, _1, _2));
    intf_.set_completer("list", boost::bind(&application::list_complete, this, _1, _2));
}

void application::run()
{
    prompt();

    while (!quit_)
    {
        try
        {
            if (!intf_.process())
                quit_ = true;
        }
        catch (usage_error& e)
        {
            std::ostringstream os;
            os << "Usage: " << e.what();
            intf_.write(os.str());
        }
    }
}

void application::prompt()
{
    pid_ = boost::none;

    std::list<gtd::gtd::project_id> l;
    
    if (context_.size())
        l = backend_.project_ids(context_);
    else
        l = backend_.project_ids();

    std::ostringstream os;

    if (!l.empty())
    {
        const gtd::project& p = backend_.get_project(l.front());
        const gtd::action& a = p.item(p.top());

        os << "next: ";
        if (p.name())
            os << '+' << *(p.name()) << ' ';

        foreach(const std::string& s, a.contexts())
            os << '@' << s << ' ';

        os << a.description();
        pid_ = l.front();
    }
    else
        os << "No items in the queue.";

    intf_.write(os.str());
}

std::vector<std::string>
application::list_complete(const std::vector<std::string>& args, bool)
{
    std::vector<std::string> result;

    if (args.size() == 2)
    {
        std::list<gtd::gtd::project_id> ids = backend_.project_ids();

        foreach(const gtd::gtd::project_id& id, ids)
            result.push_back(std::string("+") + *backend_.project_name(id));

        return rl::fetch_matches(args[1], result);
    }

    return result;
}

std::vector<std::string>
application::context_complete(const std::vector<std::string>& args, bool)
{
    std::vector<std::string> result;

    if (args.size() == 2)
    {
        result.push_back("-");

        std::set<std::string> contexts;

        std::list<gtd::gtd::project_id> p_ids = backend_.project_ids();
        foreach(const gtd::gtd::project_id& p_id, p_ids)
        {
            std::list<gtd::project::action_id> a_ids = backend_.items(p_id);
            foreach(const gtd::project::action_id& a_id, a_ids)
            {
                contexts.insert(backend_.item(p_id, a_id).contexts().begin(),
                                backend_.item(p_id, a_id).contexts().end());
            }
        }

        foreach(const std::string& context, contexts)
            result.push_back(std::string("@") + context);

        return rl::fetch_matches(args[1], result);
    }

    return result;
}

